﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using Newtonsoft.Json.Linq;

namespace TrinityAPI.Base.Models
{
    public class DynamicObjects<T> : List<T>
    {
        public override string ToString()
        {
            return Tools.JSONHelper.SerializeObject(this);
        }

        public JArray ToJArray()
        {
            return Tools.JSONHelper.JsonToJArray(Tools.JSONHelper.SerializeObject(this));
        }
    }

    public class DynamicObject : IDynamicMetaObjectProvider
    {
        private Dictionary<string, object> _dictionary = new Dictionary<string, object>();

        public DynamicObject()
        {

        }

        public DynamicObject(Dictionary<string, object> dictionary)
        {
            this._dictionary = dictionary;
        }

        public object this[string key]
        {
            get
            {
                if (!string.IsNullOrWhiteSpace(key) && _dictionary.ContainsKey(key))
                    return _dictionary[key];
                return null;
            }
            set
            {
                _dictionary[key] = value;
            }
        }

        public string[] Keys
        {
            get
            {
                return _dictionary.Keys.ToArray();
            }
        }

        public void Add(string key, object value)
        {
            _dictionary[key] = value;
        }

        public override string ToString()
        {
            return Tools.JSONHelper.SerializeObject(_dictionary);
        }

        public JObject ToJObject()
        {
            return Tools.JSONHelper.JsonToJObject(Tools.JSONHelper.SerializeObject(_dictionary));
        }

        protected virtual DynamicMetaObject GetMetaObject(Expression parameter)
        {
            return new DynamicProxyMetaObject<DynamicObject>(parameter, this, new JObjectDynamicProxy(), true);
        }

        DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
        {
            return GetMetaObject(parameter);
        }

        private class JObjectDynamicProxy : DynamicProxy<DynamicObject>
        {
            public override bool TryGetMember(DynamicObject instance, GetMemberBinder binder, out object result)
            {
                // result can be null
                result = instance[binder.Name];
                return true;
            }

            public override bool TrySetMember(DynamicObject instance, SetMemberBinder binder, object value)
            {

                instance[binder.Name] = value;
                return true;
            }

            public override IEnumerable<string> GetDynamicMemberNames(DynamicObject instance)
            {
                return instance._dictionary.Select(p => p.Key);
            }
        }


        private class DynamicProxy<T>
        {
            public virtual IEnumerable<string> GetDynamicMemberNames(T instance)
            {
                return new string[0];
            }

            public virtual bool TryBinaryOperation(T instance, BinaryOperationBinder binder, object arg, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TryConvert(T instance, ConvertBinder binder, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TryCreateInstance(T instance, CreateInstanceBinder binder, object[] args, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TryDeleteIndex(T instance, DeleteIndexBinder binder, object[] indexes)
            {
                return false;
            }

            public virtual bool TryDeleteMember(T instance, DeleteMemberBinder binder)
            {
                return false;
            }

            public virtual bool TryGetIndex(T instance, GetIndexBinder binder, object[] indexes, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TryGetMember(T instance, GetMemberBinder binder, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TryInvoke(T instance, InvokeBinder binder, object[] args, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TryInvokeMember(T instance, InvokeMemberBinder binder, object[] args, out object result)
            {
                result = null;
                return false;
            }

            public virtual bool TrySetIndex(T instance, SetIndexBinder binder, object[] indexes, object value)
            {
                return false;
            }

            public virtual bool TrySetMember(T instance, SetMemberBinder binder, object value)
            {
                return false;
            }

            public virtual bool TryUnaryOperation(T instance, UnaryOperationBinder binder, out object result)
            {
                result = null;
                return false;
            }
        }

        private sealed class DynamicProxyMetaObject<T> : DynamicMetaObject
        {
            private readonly DynamicProxy<T> _proxy;
            private readonly bool _dontFallbackFirst;

            internal DynamicProxyMetaObject(Expression expression, T value, DynamicProxy<T> proxy, bool dontFallbackFirst)
              : base(expression, BindingRestrictions.Empty, value)
            {
                _proxy = proxy;
                _dontFallbackFirst = dontFallbackFirst;
            }

            private new T Value { get { return (T)base.Value; } }

            private bool IsOverridden(string method)
            {
                return IsMethodOverridden(_proxy.GetType(), typeof(DynamicProxy<T>), method);
            }

            private bool IsMethodOverridden(Type currentType, Type methodDeclaringType, string method)
            {
                bool isMethodOverriden = currentType.GetMember(method, MemberTypes.Method, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Cast<MethodInfo>()
                  .Any(info =>
                       // check that the method overrides the original on DynamicObjectProxy
                       info.DeclaringType != methodDeclaringType
                 // todo - find out whether there is a way to do this in winrt
                 && info.GetBaseDefinition().DeclaringType == methodDeclaringType
            );

                return isMethodOverriden;
            }

            public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
            {
                return IsOverridden("TryGetMember")
                     ? CallMethodWithResult("TryGetMember", binder, NoArgs, e => binder.FallbackGetMember(this, e))
                     : base.BindGetMember(binder);
            }

            public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
            {
                return IsOverridden("TrySetMember")
                     ? CallMethodReturnLast("TrySetMember", binder, GetArgs(value), e => binder.FallbackSetMember(this, value, e))
                     : base.BindSetMember(binder, value);
            }

            public override DynamicMetaObject BindDeleteMember(DeleteMemberBinder binder)
            {
                return IsOverridden("TryDeleteMember")
                     ? CallMethodNoResult("TryDeleteMember", binder, NoArgs, e => binder.FallbackDeleteMember(this, e))
                     : base.BindDeleteMember(binder);
            }


            public override DynamicMetaObject BindConvert(ConvertBinder binder)
            {
                return IsOverridden("TryConvert")
                     ? CallMethodWithResult("TryConvert", binder, NoArgs, e => binder.FallbackConvert(this, e))
                     : base.BindConvert(binder);
            }

            public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
            {
                if (!IsOverridden("TryInvokeMember"))
                    return base.BindInvokeMember(binder, args);

                //
                // Generate a tree like:
                //
                // {
                //   object result;
                //   TryInvokeMember(payload, out result)
                //      ? result
                //      : TryGetMember(payload, out result)
                //          ? FallbackInvoke(result)
                //          : fallbackResult
                // }
                //
                // Then it calls FallbackInvokeMember with this tree as the
                // "error", giving the language the option of using this
                // tree or doing .NET binding.
                //
                Fallback fallback = e => binder.FallbackInvokeMember(this, args, e);

                DynamicMetaObject call = BuildCallMethodWithResult(
                    "TryInvokeMember",
                    binder,
                    GetArgArray(args),
                    BuildCallMethodWithResult(
                        "TryGetMember",
                        new GetBinderAdapter(binder),
                        NoArgs,
                        fallback(null),
                        e => binder.FallbackInvoke(e, args, null)
                    ),
                    null
                );

                return _dontFallbackFirst ? call : fallback(call);
            }


            public override DynamicMetaObject BindCreateInstance(CreateInstanceBinder binder, DynamicMetaObject[] args)
            {
                return IsOverridden("TryCreateInstance")
                     ? CallMethodWithResult("TryCreateInstance", binder, GetArgArray(args), e => binder.FallbackCreateInstance(this, args, e))
                     : base.BindCreateInstance(binder, args);
            }

            public override DynamicMetaObject BindInvoke(InvokeBinder binder, DynamicMetaObject[] args)
            {
                return IsOverridden("TryInvoke")
                  ? CallMethodWithResult("TryInvoke", binder, GetArgArray(args), e => binder.FallbackInvoke(this, args, e))
                  : base.BindInvoke(binder, args);
            }

            public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg)
            {
                return IsOverridden("TryBinaryOperation")
                  ? CallMethodWithResult("TryBinaryOperation", binder, GetArgs(arg), e => binder.FallbackBinaryOperation(this, arg, e))
                  : base.BindBinaryOperation(binder, arg);
            }

            public override DynamicMetaObject BindUnaryOperation(UnaryOperationBinder binder)
            {
                return IsOverridden("TryUnaryOperation")
                     ? CallMethodWithResult("TryUnaryOperation", binder, NoArgs, e => binder.FallbackUnaryOperation(this, e))
                     : base.BindUnaryOperation(binder);
            }

            public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes)
            {
                return IsOverridden("TryGetIndex")
                     ? CallMethodWithResult("TryGetIndex", binder, GetArgArray(indexes), e => binder.FallbackGetIndex(this, indexes, e))
                     : base.BindGetIndex(binder, indexes);
            }

            public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value)
            {
                return IsOverridden("TrySetIndex")
                     ? CallMethodReturnLast("TrySetIndex", binder, GetArgArray(indexes, value), e => binder.FallbackSetIndex(this, indexes, value, e))
                     : base.BindSetIndex(binder, indexes, value);
            }

            public override DynamicMetaObject BindDeleteIndex(DeleteIndexBinder binder, DynamicMetaObject[] indexes)
            {
                return IsOverridden("TryDeleteIndex")
                     ? CallMethodNoResult("TryDeleteIndex", binder, GetArgArray(indexes), e => binder.FallbackDeleteIndex(this, indexes, e))
                     : base.BindDeleteIndex(binder, indexes);
            }

            private delegate DynamicMetaObject Fallback(DynamicMetaObject errorSuggestion);

            private readonly static Expression[] NoArgs = new Expression[0];

            private static Expression[] GetArgs(params DynamicMetaObject[] args)
            {
                return args.Select(arg => Expression.Convert(arg.Expression, typeof(object))).ToArray();
            }

            private static Expression[] GetArgArray(DynamicMetaObject[] args)
            {
                return new[] { Expression.NewArrayInit(typeof(object), GetArgs(args)) };
            }

            private static Expression[] GetArgArray(DynamicMetaObject[] args, DynamicMetaObject value)
            {
                return new Expression[]
                      {
                Expression.NewArrayInit(typeof(object), GetArgs(args)),
                Expression.Convert(value.Expression, typeof(object))
                      };
            }

            private static ConstantExpression Constant(DynamicMetaObjectBinder binder)
            {
                Type t = binder.GetType();
                while (!t.IsVisible)
                    t = t.BaseType;
                return Expression.Constant(binder, t);
            }

            /// <summary>
            /// Helper method for generating a MetaObject which calls a
            /// specific method on Dynamic that returns a result
            /// </summary>
            private DynamicMetaObject CallMethodWithResult(string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback, Fallback fallbackInvoke = null)
            {
                //
                // First, call fallback to do default binding
                // This produces either an error or a call to a .NET member
                //
                DynamicMetaObject fallbackResult = fallback(null);

                DynamicMetaObject callDynamic = BuildCallMethodWithResult(methodName, binder, args, fallbackResult, fallbackInvoke);

                //
                // Now, call fallback again using our new MO as the error
                // When we do this, one of two things can happen:
                //   1. Binding will succeed, and it will ignore our call to
                //      the dynamic method, OR
                //   2. Binding will fail, and it will use the MO we created
                //      above.
                //

                return _dontFallbackFirst ? callDynamic : fallback(callDynamic);
            }

            private DynamicMetaObject BuildCallMethodWithResult(string methodName, DynamicMetaObjectBinder binder, Expression[] args, DynamicMetaObject fallbackResult, Fallback fallbackInvoke)
            {
                //
                // Build a new expression like:
                // {
                //   object result;
                //   TryGetMember(payload, out result) ? fallbackInvoke(result) : fallbackResult
                // }
                //
                ParameterExpression result = Expression.Parameter(typeof(object), null);

                IList<Expression> callArgs = new List<Expression>();
                callArgs.Add(Expression.Convert(Expression, typeof(T)));
                callArgs.Add(Constant(binder));
                foreach (var arg in args)
                    callArgs.Add(arg);
                callArgs.Add(result);

                DynamicMetaObject resultMetaObject = new DynamicMetaObject(result, BindingRestrictions.Empty);

                // Need to add a conversion if calling TryConvert
                if (binder.ReturnType != typeof(object))
                {
                    UnaryExpression convert = Expression.Convert(resultMetaObject.Expression, binder.ReturnType);
                    // will always be a cast or unbox

                    resultMetaObject = new DynamicMetaObject(convert, resultMetaObject.Restrictions);
                }

                if (fallbackInvoke != null)
                    resultMetaObject = fallbackInvoke(resultMetaObject);

                DynamicMetaObject callDynamic = new DynamicMetaObject(
                  Expression.Block(
                    new[] { result },
                    Expression.Condition(
                      Expression.Call(
                        Expression.Constant(_proxy),
                        typeof(DynamicProxy<T>).GetMethod(methodName),
                        callArgs
                        ),
                      resultMetaObject.Expression,
                      fallbackResult.Expression,
                      binder.ReturnType
                      )
                    ),
                  GetRestrictions().Merge(resultMetaObject.Restrictions).Merge(fallbackResult.Restrictions)
                  );

                return callDynamic;
            }

            /// <summary>
            /// Helper method for generating a MetaObject which calls a
            /// specific method on Dynamic, but uses one of the arguments for
            /// the result.
            /// </summary>
            private DynamicMetaObject CallMethodReturnLast(string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback)
            {
                //
                // First, call fallback to do default binding
                // This produces either an error or a call to a .NET member
                //
                DynamicMetaObject fallbackResult = fallback(null);

                //
                // Build a new expression like:
                // {
                //   object result;
                //   TrySetMember(payload, result = value) ? result : fallbackResult
                // }
                //
                ParameterExpression result = Expression.Parameter(typeof(object), null);

                IList<Expression> callArgs = new List<Expression>();
                callArgs.Add(Expression.Convert(Expression, typeof(T)));
                callArgs.Add(Constant(binder));
                foreach (var arg in args)
                    callArgs.Add(arg);
                callArgs[args.Length + 1] = Expression.Assign(result, callArgs[args.Length + 1]);

                DynamicMetaObject callDynamic = new DynamicMetaObject(
                    Expression.Block(
                        new[] { result },
                        Expression.Condition(
                            Expression.Call(
                                Expression.Constant(_proxy),
                                typeof(DynamicProxy<T>).GetMethod(methodName),
                                callArgs
                            ),
                            result,
                            fallbackResult.Expression,
                            typeof(object)
                        )
                    ),
                    GetRestrictions().Merge(fallbackResult.Restrictions)
                );

                //
                // Now, call fallback again using our new MO as the error
                // When we do this, one of two things can happen:
                //   1. Binding will succeed, and it will ignore our call to
                //      the dynamic method, OR
                //   2. Binding will fail, and it will use the MO we created
                //      above.
                //
                return _dontFallbackFirst ? callDynamic : fallback(callDynamic);
            }

            /// <summary>
            /// Helper method for generating a MetaObject which calls a
            /// specific method on Dynamic, but uses one of the arguments for
            /// the result.
            /// </summary>
            private DynamicMetaObject CallMethodNoResult(string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback)
            {
                //
                // First, call fallback to do default binding
                // This produces either an error or a call to a .NET member
                //
                DynamicMetaObject fallbackResult = fallback(null);

                IList<Expression> callArgs = new List<Expression>();
                callArgs.Add(Expression.Convert(Expression, typeof(T)));
                callArgs.Add(Constant(binder));
                foreach (var arg in args)
                    callArgs.Add(arg);

                //
                // Build a new expression like:
                //   if (TryDeleteMember(payload)) { } else { fallbackResult }
                //
                DynamicMetaObject callDynamic = new DynamicMetaObject(
                  Expression.Condition(
                    Expression.Call(
                      Expression.Constant(_proxy),
                      typeof(DynamicProxy<T>).GetMethod(methodName),
                      callArgs
                      ),
                    Expression.Empty(),
                    fallbackResult.Expression,
                    typeof(void)
                    ),
                  GetRestrictions().Merge(fallbackResult.Restrictions)
                  );

                //
                // Now, call fallback again using our new MO as the error
                // When we do this, one of two things can happen:
                //   1. Binding will succeed, and it will ignore our call to
                //      the dynamic method, OR
                //   2. Binding will fail, and it will use the MO we created
                //      above.
                //
                return _dontFallbackFirst ? callDynamic : fallback(callDynamic);
            }

            /// <summary>
            /// Returns a Restrictions object which includes our current restrictions merged
            /// with a restriction limiting our type
            /// </summary>
            private BindingRestrictions GetRestrictions()
            {
                return (Value == null && HasValue)
                     ? BindingRestrictions.GetInstanceRestriction(Expression, null)
                     : BindingRestrictions.GetTypeRestriction(Expression, LimitType);
            }

            public override IEnumerable<string> GetDynamicMemberNames()
            {
                return _proxy.GetDynamicMemberNames(Value);
            }

            // It is okay to throw NotSupported from this binder. This object
            // is only used by DynamicObject.GetMember--it is not expected to
            // (and cannot) implement binding semantics. It is just so the DO
            // can use the Name and IgnoreCase properties.
            private sealed class GetBinderAdapter : GetMemberBinder
            {
                internal GetBinderAdapter(InvokeMemberBinder binder) :
                  base(binder.Name, binder.IgnoreCase)
                {
                }

                public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
                {
                    throw new NotSupportedException();
                }
            }
        }
    }
}